﻿using System;
using System.IO;
using System.Linq;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.Checksums;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace SourceBackup
{
    public delegate void DBackupFeedback(string s);

    #region " interfaces "
    public interface IDirBackup
    {
        bool DoBackup(string zipfilename, string Directory2Backup);
    }
    #endregion

    /// <summary>
    /// Summary description for DirBackup.
    /// </summary>
    public class DirBackup : IDirBackup
    {
        private string zipfilename = "";

        #region Properties
        public StringCollection FileFilters { get; private set; }
        public bool ShowIgnoredFiles { get; set; }
        public bool IgnoreBinObjDirs { get; set; }
        private int filesZipped = 0;
        public int FilesZipped
        {
            get { return filesZipped; }
        }
        private long totalUncompressedSizeBytes = 0;
        public long TotalUncompressedSizeKB
        {
            get { return totalUncompressedSizeBytes / 1024; }
        }
        private long totalCompressedSizeBytes = 0;
        public long TotalCompressedSizeKB
        {
            get { return totalCompressedSizeBytes / 1024; }
        }
        public List<string> ExcludedFileTypes
        {
            get;
            internal set;
        }
        #endregion

        #region Contructors
        public DirBackup()
        {
            FileFilters = new StringCollection();
            ExcludedFileTypes = new List<string>();
        } 
        #endregion

        #region " events "
        public event DBackupFeedback Feedback;
        public event DBackupFeedback FeedbackBold;
        public event DBackupFeedback FeedbackNoRCLF;
        private void GiveFeedback(string s)
        {
            if (Feedback != null)
            {
                Feedback(s);
            }
        }
        private void GiveFeedbackBold(string s)
        {
            if (FeedbackBold != null)
            {
                FeedbackBold(s);
            }
        }
        private void GiveFeedbackNoCRLF(string s)
        {
            if (FeedbackNoRCLF != null)
            {
                FeedbackNoRCLF(s);
            }
        }
        public event DBackupFeedback FeedbackWarning;
        private void GiveFeedbackWarning(string s)
        {
            if (FeedbackWarning != null)
            {
                FeedbackWarning(s);
            }
        }
        #endregion

        #region " public methods "
        public bool DoBackup(string zipfilename, string Directory2Backup)
        {
            try
            {
                this.zipfilename = zipfilename;
                if ((zipfilename == null) || (zipfilename == ""))
                {
                    throw new Exception("No zip file name specified!");
                }
                if ((Directory2Backup == null) || (Directory2Backup == ""))
                {
                    throw new Exception("No directory name specified!");
                }
                if (FileFilters.Count == 0)
                {
                    throw new Exception("No file filter(s) specified!");
                }

                string rootdir = Directory2Backup;
                if (rootdir.EndsWith("\\"))
                {
                    rootdir = rootdir.Substring(0, rootdir.Length - 1);
                }
                using (FileStream fs = File.Create(zipfilename))
                {
                    using (ZipOutputStream s = new ZipOutputStream(fs))
                    {
                        filesZipped = 0;
                        totalUncompressedSizeBytes = 0;
                        totalCompressedSizeBytes = 0;
                        s.SetLevel(9); // 0 - store only to 9 - means best compression
                        BackupDir(s, Directory2Backup, rootdir);
                        s.SetComment("Source code backup of directory: " + Directory2Backup);
                        s.Finish();
                        s.Close();
                    }
                }
                System.Threading.Thread.Sleep(10);
            }
            catch 
            {
                throw;
            }
            return true;
        }
        #endregion

        #region " private methods "
        private List<string> GetFileNames(string directoryPath)
        {
            List<string> fileNames = new List<string>();
            foreach (string fileFilter in FileFilters)
            {
                string fullFileFilter = fileFilter;
                bool exclude = fullFileFilter.StartsWith("~");
                fullFileFilter = fullFileFilter.TrimStart('~');

                #region Set full file filter
                if (!fullFileFilter.Contains("*"))
                {
                    fullFileFilter = "*" + fullFileFilter;
                }
                #endregion

                string[] files = Directory.GetFiles(directoryPath, fullFileFilter);
                foreach (string fileName in files)
                {
                    if (fileName == zipfilename)
                        continue;
                    if (exclude && fileNames.Contains(fileName))
                    {
                        fileNames.Remove(fileName);
                    }
                    else if (!exclude && !fileNames.Contains(fileName))
                    {
                        fileNames.Add(fileName);
                    }
                }
            }
            return fileNames;
        }
        private List<string> GetNotIgnoredFiles(string directoryPath)
        {
            List<string> fileNames = new List<string>();
            //first list all files
            string[] files = Directory.GetFiles(directoryPath, "*.*");
            foreach (string fileName in files)
            {
                if (fileName == zipfilename)
                    continue;
                fileNames.Add(fileName);
            }

            foreach (string fileFilter in (from string s in FileFilters
                                           where s.StartsWith("~")
                                           select s))
            {
                string fullFileFilter = fileFilter;
                fullFileFilter = fullFileFilter.TrimStart('~');

                #region Set full file filter
                if (!fullFileFilter.Contains("*"))
                {
                    fullFileFilter = "*" + fullFileFilter;
                }
                #endregion

                files = Directory.GetFiles(directoryPath, fullFileFilter);
                foreach (string fileName in files)
                {
                    if (fileName == zipfilename)
                        continue;
                    if (fileNames.Contains(fileName))
                    {
                        fileNames.Remove(fileName);
                    }
                }
            }

            return fileNames;
        }

        private void BackupDir(ZipOutputStream s, string Directory2Backup, string RootDirectory)
        {
            if (IgnoreBinObjDirs)
            {
                if (Directory2Backup.ToLower().EndsWith("\\bin") ||
                    Directory2Backup.ToLower().EndsWith("\\obj") ||
                    Directory2Backup.ToLower().EndsWith("\\release") ||
                    Directory2Backup.ToLower().EndsWith("\\debug")
                    )
                    return;
            }

            Crc32 crc = new Crc32();
            List<string> filesToBackup = GetFileNames(Directory2Backup);
            foreach (string file in filesToBackup)
            {
                try
                {
                    FileInfo fi = new FileInfo(file);
                    if (Properties.Settings.Default.DisplayExtraFileInfo)
                        GiveFeedback(string.Format("{0}\r\n   Created {1}, Modified {2}, Attr {3}", file, fi.CreationTime, fi.LastWriteTime, FormatFileAttr(fi.Attributes)));
                    else
                        GiveFeedback(file);
                    using (FileStream fs = fi.OpenRead())
                    {
                        byte[] buffer = new byte[fs.Length];
                        fs.Read(buffer, 0, buffer.Length);
                        ZipEntry entry = new ZipEntry(file.Substring(RootDirectory.Length + 1));

                        // set Size and the crc, because the information
                        // about the size and crc should be stored in the header
                        // if it is not set it is automatically written in the footer.
                        // (in this case size == crc == -1 in the header)
                        // Some ZIP programs have problems with zip files that don't store
                        // the size and crc in the header.
                        entry.Size = fs.Length;
                        entry.DateTime = fi.LastWriteTime; // File.GetLastWriteTime(file);
                        entry.Comment = file;
                        fs.Close();

                        crc.Reset();
                        crc.Update(buffer);
                        entry.Crc = crc.Value;

                        s.PutNextEntry(entry);
                        s.Write(buffer, 0, buffer.Length);
                        s.CloseEntry();
                        GiveFeedbackBold("   Compressed: " + FormatBytes(entry.Size) + " -> " + FormatBytes(entry.CompressedSize));

                        totalUncompressedSizeBytes += entry.Size;
                        totalCompressedSizeBytes += entry.CompressedSize;
                        filesZipped++;
                        entry = null;
                    }
                }
                catch (Exception ex)
                {
                    GiveFeedbackWarning(" error -> " + ex.Message);
                }
            }
            if (ShowIgnoredFiles)
                foreach (string file in GetNotIgnoredFiles(Directory2Backup))
                {
                    if (!filesToBackup.Contains(file))
                    {
                        string extn = Path.GetExtension(file);
                        if (!ExcludedFileTypes.Contains(extn))
                            ExcludedFileTypes.Add(extn);
                        GiveFeedbackWarning("Ignoring file: " + file);
                    }
                }
           
            crc = null;
            string[] directories;
            directories = Directory.GetDirectories(Directory2Backup);
            foreach (string directory in directories)
            {
                BackupDir(s, directory, RootDirectory);
            }
        }
        #endregion

        #region Formatting output
        /// <summary>
        /// Format a value to file size (Byte/KB/MB...)
        /// </summary>
        /// <param name="byteSize">Value to format</param>
        /// <returns>Formatted string</returns>
        private string FormatBytes(long byteSize)
        {
            if (byteSize < 1024)
                return byteSize.ToString() + " Byte(s)";
            else if (byteSize < 1048576)
                return (byteSize / 1024).ToString() + " KB";
            else if (byteSize < 1073741824)
                return (byteSize / 1048576).ToString() + " MB";
            else
                return (byteSize / 1073741824).ToString() + " GB";
        }
        private string FormatFileAttr(FileAttributes fa)
        {
            string s = "";
            if ((fa & FileAttributes.Archive) == FileAttributes.Archive)
                s += "A";
            if ((fa & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                s += "R";
            if ((fa & FileAttributes.Hidden) == FileAttributes.Hidden)
                s += "H";
            if ((fa & FileAttributes.System) == FileAttributes.System)
                s += "S";
            if ((fa & FileAttributes.Compressed) == FileAttributes.Compressed)
                s += "C";
            return s;
        } 
        #endregion
    }
}
